import base64
import copy
import os.path
import pickle
import StringIO
import sys
import zipfile

import AreaInfo
import graphics
#import guiutils
import ImageReader
import io
import Player
import Unit
import URStack
import utils
import UType

DEFAULT_MAP_FILENAME="map.gif"
DEFAULT_AREAS_FILENAME="areas.aes"
DEFAULT_SETUP_FILENAME="setup.stp"
DEFAULT_GAME_FILENAME="game.zip"
GAMEMASTER="Game master"

def load_from_archive(archive, fname, func, prefix):
    if fname.find(prefix) == 0:
        func(StringIO.StringIO(archive.read(fname)))
        return True
    return False

def load_gameconf_from_archive(filename, root=None):
    ziparchive = None
    newconf = GameConfiguration()
    errors = []
    try:
        ziparchive = zipfile.ZipFile(filename, 'r')
        files = ziparchive.namelist()

        for fname in files:
            
            if fname in ["map.ppm", "map.gif"]:
                newconf.mem_load_map(ziparchive.read(fname))
                if not newconf.has_map():
                    errors.append("Could not load %s from zip archive." %
                                  fname)

            elif fname in ["areas.aes"]:
                if not newconf.get_area_info().load_areas_file_content(
                    ziparchive.read("areas.aes")):
                    errors.append("Could not load areas.aes from zip archive.")

            elif fname.startswith("bitmap"):
                bitmap_data = ziparchive.read(fname)
                if ((fname in newconf.bitmaps) and 
                    (newconf.bitmaps[fname] != bitmap_data)):
                    errors.append("Bitmap '%s' redefined, some units' bitmap might be incorrect." % fname)
                    bitmap_data = newconf.bitmaps[fname]
                newconf.bitmaps[fname] = bitmap_data

            elif load_from_archive(
                ziparchive, 
                fname, 
                newconf.load_players_file_contents,
                "players"):
                pass

            elif load_from_archive(
                ziparchive, 
                fname, 
                newconf.load_colours_file_contents,
                "colours"):
                pass
            
            elif load_from_archive(
                ziparchive, 
                fname, 
                newconf.load_unit_types_file_contents,
                "unittypes"):
                pass
            
            elif load_from_archive(
                ziparchive, 
                fname, 
                newconf.load_setup_file_contents,
                "setup"):
                pass
            
            elif load_from_archive(
                ziparchive,
                fname,
                newconf.load_confname_file_contents,
                "confname"):
                pass
            
            elif load_from_archive(
                ziparchive,
                fname,
                newconf.load_undo_stack,
                "undostack"):
                pass
            
            elif load_from_archive(
                ziparchive,
                fname,
                newconf.load_redo_stack,
                "redostack"):
                pass
            
            else:
                errors.append("%s contains unrecognized file: %s" % 
                              (filename, fname))

    except (IOError, zipfile.error, KeyError), e:
        errors.append(str(e))
        newconf = None
    finally:
        if ziparchive is not None:
            ziparchive.close()
    return newconf, errors


def default_xbm_path():
    return os.path.realpath(os.path.join(utils.mdpath(), "..", "bitmaps", "default.xbm"))

# TODO: this shall be removed (it adds attributes that might be useful for wa3r only)
def fix_unit_attr_typos(attrs):
    if 'Att' in attrs:
        defpos = attrs['Att'].find('Def')
        if defpos>1:
            defvalue = (attrs['Att'][defpos+3:]).strip()
            attrs['Att'] = (attrs['Att'][0:defpos]).strip()
            attrs['Def'] = defvalue
    if 'Quality' not in attrs:
        attrs['Quality'] = 'standard'
        
        if 'new' in attrs:
            qtypos = attrs['new'].find('Inferior')
            if qtypos>-1:
                attrs['Quality'] = 'inferior'
    if 'new' in attrs:
        del attrs['new']

class GameConfiguration:
    
    @classmethod
    def default_colours(cls):
        return [ 
            ("SlateGray4", "ghostwhite"),
            ("VioletRed4", "GhostWhite"),
            ("goldenrod3", "grey"),
            ("darkgreen", "grey"),
            ("RoyalBlue", "GhostWhite"),
            ("red3", "ghostwhite"),
            ]

    default_name = "<No name>"
    
    def __init__(self):
        self.ainfo = AreaInfo.AreaInfo()
        self.utypes = {}
        self.colours = GameConfiguration.default_colours()
            
        self.units = {}
        self.game_filename = None
        self.map_reader = ImageReader.ImageReader()
        self.map_filename = DEFAULT_MAP_FILENAME
        gm = Player.Player("Game Master")
        self.players = [gm]

        # mapping between bitmap names  and bitmap data
        self.bitmaps = utils.O2ODict()
        f = open(default_xbm_path(), 'r')
            
        bitmap_0 = self.get_bitmap_name(f.read())
        assert bitmap_0 == "bitmap_0", bitmap_0
        f.close()

        # if self.game_password == "plain", setup files are
        # saved as plain text
        self.game_password = "plain"
        self.name = GameConfiguration.default_name
        self.ur_stack = URStack.UndoRedoStack()

    def __copy__(self):
        gconf = GameConfiguration()
        gconf.ainfo = copy.copy(self.ainfo)
        gconf.colours = copy.copy(self.colours)
        gconf.utypes = utils.copydict(self.utypes)
        gconf.units = utils.copydict(self.units)
        gconf.game_filename = self.game_filename
        gconf.map_reader = copy.copy(self.map_reader)
        gconf.map_filename = self.map_filename
        gconf.players = copy.copy(self.players)
        gconf.bitmaps = copy.copy(self.bitmaps)
        gconf.game_password = self.game_password
        gconf.name = self.name
        gconf.ur_stack = copy.copy(self.ur_stack)
        return gconf

    def get_unit_type(self, utypename):
        return self.utypes.get(utypename, None)

    def get_unit_types(self):
        return self.utypes.values()

    def delete_unit_type(self, utpname):
        if utpname in self.utypes:
            del self.utypes[utpname]
            
    def add_unit_type(self, utp):
        self.utypes[utp.name] = utp
        if "bitmap" not in utp.attrs:
            sys.stderr.write("UnitType without bitmap %s\n" % utp.name)

    def get_sorted_unit_types(self):
        terrains = {}
        for ut in self.utypes.values():
            t = ut.attrs.get("terrain", "any")
            if t not in terrains:
                terrains[t] = []
            terrains[t].append(ut)
        for t in terrains:
            terrains[t].sort()
        return terrains
        
    def has_units(self):
        return len(self.units)>0
    
    def get_units(self):
        return self.units.values()
    
    def get_unit_names(self):
        return self.units.keys()
    
    def get_unit(self, uname):
        return self.units.get(uname, None)
    
    def add_unit(self, unit):
        self.units[unit.name] = unit
        if "bitmap" not in unit.attrs:
            sys.stderr.write("Unit without bitmap: %s\n" % unit.name)
    
    def del_unit(self, unit):
        if unit.name in self.units:
            del self.units[unit.name]
            
    def get_area_info(self):
        '''
        Returns the AreaInfo.
        '''
        return self.ainfo

    def get_areas(self):
        return self.get_area_info().get_areas()
    
    def get_neighbours(self, aname):
        return self.get_area_info().get_neighbours(aname)
    
    def has_players(self):
        return len(self.players)>1
     
    def get_players(self):
        return self.players
         
    def colours_to_string(self):
        return ",".join([":".join(fgbg) for fgbg in self.colours])
    
    def load_colours_file_contents(self, inputfile):
        lines = inputfile.readlines()
        colours = []
        for line in lines:
            colours.extend([tuple(fgbg.split(":")) for fgbg in line.split(",")])
        if len(colours) == 0:
            colours = GameConfiguration.default_colours()
        self.colours = colours

    def make_bitmap_name(self):
        return "bitmap_%d" % len(self.bitmaps)

    def get_bitmap_name(self, bitmap_data, default=None):
        if bitmap_data not in self.bitmaps.inv:
            if default is None:
                name = self.make_bitmap_name()
            self.bitmaps[name] = bitmap_data
        return self.bitmaps.inv[bitmap_data]

    def get_default_unit_bitmap(self):
        return "bitmap_0"

    def get_unit_bitmap_data(self, u):
        return self.bitmaps[u.attrs['bitmap']]

    def make_unit_name(self, name, seqn):
        return "%s-%04d" % (name, seqn)

    def unique_unit_seq_number(self, unit_name, seqnum=0):
        while True:
            nm = self.make_unit_name(unit_name, seqnum)
            if nm not in self.units:
                return seqnum
            seqnum += 1
        return -1
        
    def make_unit_name_unique(self, unit_name):
        seqnum = 0
        nm = self.make_unit_name(unit_name, seqnum)
        while nm in self.units:
            nm = self.make_unit_name(unit_name, seqnum)
            seqnum += 1
        return nm

    def is_plain_text_setup(self, txt):
        stxt = txt.strip()
        if len(stxt)==0:
            return True
        return stxt.find("begin Unit") >= 0

    def load_setup_file_contents(self, inputfile):
        encodedInput = inputfile.read()
        if self.is_plain_text_setup(encodedInput):
            decodedInput = encodedInput
        else:
            scrambler = io.Scrambler(key=self.game_password)
            decodedInput = scrambler.encode(encodedInput)
        def process_unit(attrs, idx0, idx1):
            # TODO: this is a hack to
            # fix misspelled attributes
            # based on the current wa3r game.
            fix_unit_attr_typos(attrs)

            u = self.make_unit(attrs, idx0, idx1)
            if u is not None:
                self.add_unit(u)
        self.parse_setup_file_contents(decodedInput, process_unit)

    def load_unit_types_file_contents(self, inputfile):
        def process_unit_type(attrs, idx0, idx1):
            utp = self.make_unit_type(attrs, idx0, idx1)
            if utp is not None:
                self.add_unit_type(utp)
        self.parse_setup_file_contents(inputfile.read(), process_unit_type)

    def load_confname_file_contents(self, inputfile):
        self.name = inputfile.readline().strip()
        
    def make_unit_type(self, attrs, idx0, idx1):
        if "name" not in attrs:
            attrs['name'] = 'UnnamedUnitType'

        if ('bitmap' not in attrs) and ('bitmap_data' not in attrs):
            attrs['bitmap'] = self.get_default_unit_bitmap()

        name = attrs["name"]
        del attrs["name"]
        if 'bitmap' not in attrs:
            bitmap_data = self.bitmap_data_from_attrs(attrs)
            if bitmap_data is not None:
                bitmap_name = self.get_bitmap_name(bitmap_data)
            else:
                bitmap_name = self.get_default_unit_bitmap()
            attrs['bitmap'] = bitmap_name  
        utp = UType.UnitType(name, copy.copy(attrs))
        return utp

    def bitmap_data_from_attrs(self, attrs):
        bitmap_data = None
        if "bitmap_data" in attrs:
            bitmap_data = base64.b64decode(attrs["bitmap_data"])
            del attrs["bitmap_data"]
        return bitmap_data
        
    def get_unit_types_file_contents(self):
        utps = self.get_unit_types()
        utps.sort()
        return "\n".join([utp.get_save_str() for utp in utps])

    def parse_setup_file_contents(self, setup_contents, process_unit_func):
        lines = utils.read_lines(StringIO.StringIO(setup_contents), eolstr="")
        unit_parts = {}
        unit_parts_lines =[]
        lnum = 1
        idx0 = 1
        for line in lines:
            stripped_line = line.strip()
            if len(line)>0:
                if stripped_line.startswith("end Unit "):
                    attrs = "".join(unit_parts_lines)
                    attrs = io.read_attrs(attrs)
                    unit_parts.update(attrs)
                    process_unit_func(unit_parts, idx0, lnum)
                    idx0 = lnum+1
                elif stripped_line.startswith("begin Unit"):
                    unit_parts = {}
                    unit_parts["name"] = (stripped_line[len("begin Unit "):]).strip()
                    unit_parts_lines =[]         
                    idx0 = lnum
                else:
                    unit_parts_lines.append(line)
            lnum += 1
        

    def load_setup(self, filename):
        result = True
        try:
            inputfile = file(filename, 'r')
            self.load_setup_file_contents(inputfile)
        except IOError, e:
            result = False
        inputfile.close()
        return result

    def required_unit_attrs(self):
        return Unit.Unit.required_attrs()

    def has_required_unit_attrs(self, attrs):
        for a in self.required_unit_attrs():
            if attrs.get(a, "") == "":
                return False
        return True

    def make_unit(self, unit_parts, idx0, idx1):
        unit_keys = self.required_unit_attrs()
        missing_keys = [k for k in unit_keys if k not in unit_parts]
        if len(missing_keys)>0:
            #guiutils.display_error_message(
            #    ("Lines %d-%d: Incomplete unit specification" % (idx0, idx1))+
            #    "\nMissing:  %s" % (", ".join(missing_keys)))
            return None

        else:
            errors = []
            try:
                x, y = eval(unit_parts["location"])
            except SyntaxError:
                errors.append(
                    "Lines %d-%d: Incorrect (x,y) coordinates"+
                    (":\nlocation: %s\n" % unit_parts["location"]))

            move = unit_parts.get("move",None)
            moves = []
            if move is not None:
                try:
                    moves = [eval(p) for p in move.split("->")]
                except SyntaxError:
                    errors.append(
                        "Incorrect move for unit" +
                        (":'move: %s'" % unit_parts["move"]))
                if len(moves)>0 and moves[0] != (x, y):
                    errors.append(
                        "Move does not start at unit's location"+
                        (":\nmove start=%s, unit location=(%d, %d)"%
                         (str(moves[0]), x, y)))
            else:
                moves = [(x,y)]
            unit_parts["move"] = moves
            if len(errors)>0:
                #guiutils.display_error_message(
                #    ("Lines %d-%d: Errors in unit specification:\n" %
                #     (idx0, idx1))+
                #    "\n".join(errors))
                return None
            else:
                bitmap_data = self.bitmap_data_from_attrs(unit_parts)
                if bitmap_data is not None:
                    bitmap_name = self.get_bitmap_name(bitmap_data)
                    unit_parts['bitmap'] = bitmap_name
                if 'bitmap' not in unit_parts:
                    unit_parts['bitmap'] = self.get_default_unit_bitmap()
                unit = self.create_unit_from_dict(unit_parts)
            return unit

    def create_unit_from_dict(self, attrs):
        x, y = eval(attrs["location"])
        unit = Unit.Unit(
            attrs["name"],
            attrs["unittype"],
            attrs["color"],
            attrs["bgcolor"],
            x,
            y)
        unit.move_dest = attrs["move"]
        del attrs["move"]
        unit_keys = self.required_unit_attrs()
        for k in attrs:
            if k not in unit_keys:
                unit.attrs[k] = attrs[k]
        return unit
            
    def get_setup_file_contents(self):
        units = self.get_units()
        units.sort()
        scrambler = io.Scrambler(key=self.game_password)
        encodedOutput = scrambler.encode(
            "\n".join([u.get_save_str() for u in units]))
        return encodedOutput


    def load_players_file_contents(self, inputfile):
        lines = utils.read_lines(inputfile, eolstr="")
        player_parts = {}
        player_parts_lines =[]
        lnum = 1
        idx0 = 1
        self.players = []
        for line in lines:
            stripped_line = line.strip()
            if len(line)>0:
                if stripped_line.startswith("end Player"):
                    attrs = "".join(player_parts_lines)
                    attrs = io.read_attrs(attrs)
                    player_parts.update(attrs)
                    self.players.append(
                        self.make_player(player_parts, idx0, lnum))
                    idx0 = lnum+1
                elif stripped_line.startswith("begin Player"):
                    player_parts = {}
                    player_parts_lines =[]         
                    idx0 = lnum
                else:
                    player_parts_lines.append(line)
            lnum += 1
        return True

    def make_player(self, attrs, idx0, idx1):
        if "name" not in attrs:
            attrs['name'] = "UnnamedPlayer"
        name = attrs["name"]
        del attrs["name"]
        if "password" in attrs:
            password = attrs["password"].strip()
            if password == "None":
                password = None
            del attrs["password"]
            
        player = Player.Player(name)
        if password is None:
            player.password = None
        else:
            player.set_password_from_save_str(password)
        
        player.attrs = attrs
        return player

    def colour_attr_to_fgbg(self, colour):
        return tuple(colour.split(":"))

    def get_players_file_content(self):
        return "\n".join([p.get_save_str() for p in self.players])

    def mem_load_map(self, map_bytes):
        self.map_reader.set_image_data(map_bytes)
        
    def get_map_img_data(self):
        return self.map_reader.get_image_data()

    def has_map(self):
        return self.map_reader.has_data()

    def get_map_filename(self):
        return self.map_filename

    def set_map_filename(self, map_filename):
        self.map_filename = map_filename

    def save(self, filename=None, undostack=None, redostack=None):
        errors = []
        if filename is None:
            filename = self.game_filename
        if filename is None:
            errors.append("No file name specified.")
            return errors
        try:
            archive = zipfile.ZipFile(filename, 'w', zipfile.ZIP_DEFLATED)
            if self.has_map():
                archive.writestr(self.get_map_filename(), 
                                 self.get_map_img_data())
            bmps = self.bitmaps.keys()
            bmps.sort()
            for b in bmps:
                archive.writestr(b, self.bitmaps[b])
            archive.writestr(
                "areas.aes", 
                self.get_area_info().get_areas_file_content())
            cnf = self
            turn_num = 1
            archive.writestr("players_%d.pla" % turn_num,
                             cnf.get_players_file_content())
            archive.writestr("colours_%d.clr" % turn_num,
                             cnf.colours_to_string())
            archive.writestr("unittypes_%d.utp" % turn_num, 
                             cnf.get_unit_types_file_contents())
            archive.writestr("setup_%d.stp" % turn_num,
                             cnf.get_setup_file_contents())
            archive.writestr("confname.txt", self.name)
            archive.writestr("undostack.bin", 
                             pickle.dumps(undostack, pickle.HIGHEST_PROTOCOL))
            archive.writestr("redostack.bin", 
                             pickle.dumps(redostack, pickle.HIGHEST_PROTOCOL))
            
        except (IOError, zipfile.error), e:
            errors.append("Could not save to %s: %s" % (filename, e))
            return errors
        if archive is not None:
            archive.close()
        self.game_filename = filename
        return errors

    def areas_for_pos(self, x, y):
        result = []
        for a in self.ainfo.areas.values():
            if graphics.inside_polygon((x, y), a.polygon, edge = False):
                result.append(a)
        return result

    def area_for_pos(self, x, y):
        areas = self.areas_for_pos(x, y)
        if len(areas) == 0:
            return None
        a = areas[0]
        bbarea = graphics.bbox_area(a.bbox())
        for a1 in areas[1:]:
            bbarea1 = graphics.bbox_area(a1.bbox())
            if bbarea > bbarea1:
                a = a1
                bbarea = bbarea1
        return a
        
    def areas_for_unit(self, unit):
        return self.areas_for_pos(unit.x, unit.y)
    
    def area_for_unit(self, unit):
        return self.area_for_pos(unit.x, unit.y)

    def load_undo_stack(self, inputfile):
        self.ur_stack.undo_stack = pickle.load(inputfile)
        
    def load_redo_stack(self, inputfile):
        self.ur_stack.redo_stack = pickle.load(inputfile)
